<!DOCTYPE HTML>
<html>
<head>
  <meta charset=utf-8>
  <title>unicode-range load tests using font loading api</title>
  <link rel="author" title="John Daggett" href="mailto:jdaggett@mozilla.com">
  <link rel="help" href="http://www.w3.org/TR/css-fonts-3/#unicode-range-desc" />
  <link rel="help" href="http://dev.w3.org/csswg/css-font-loading/" />
  <meta name="assert" content="unicode-range descriptor defines precisely which fonts should be loaded" />
  <script type="text/javascript" src="/resources/testharness.js"></script>
  <script type="text/javascript" src="/resources/testharnessreport.js"></script>
  <style type="text/css">
  </style>
</head>
<body>
<div id="log"></div>
<pre id="display"></pre>
<style id="testfonts"></style>
<style id="teststyle"></style>
<div id="testcontent"></div>

<script>

const kSheetFonts = 1;
const kSheetStyles = 2;

const redSquDataURL = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10' width='100%' height='100%'><rect fill='red' x='0' y='0' width='10' height='10'/></svg>";

var unicodeRangeTests = [
  { test: "simple load sanity check, unused fonts not loaded",
    fonts: [{ family: "a", src: "markA", descriptors: { }, loaded: false}],
    content: "AAA", style: { "font-family": "unused" } },
  { test: "simple load sanity check, font for a used character loaded",
    fonts: [{ family: "a", src: "markA", descriptors: { unicodeRange: "u+41" }, loaded: true}],
    content: "AAA" },
  { test: "simple load sanity check, font for an unused character not loaded",
    fonts: [{ family: "a", src: "markA", descriptors: { unicodeRange: "u+41" }, loaded: false}],
    content: "BBB" },
  { test: "simple load sanity check, with two fonts only font for used character loaded A",
    fonts: [{ family: "a", src: "markA", descriptors: { unicodeRange: "u+41" }, loaded: true},
            { family: "a", src: "markB", descriptors: { unicodeRange: "u+42" }, loaded: false}],
    content: "AAA" },
  { test: "simple load sanity check, with two fonts only font for used character loaded B",
    fonts: [{ family: "a", src: "markA", descriptors: { unicodeRange: "u+41" }, loaded: false},
            { family: "a", src: "markB", descriptors: { unicodeRange: "u+42" }, loaded: true}],
    content: "BBB" },
  { test: "simple load sanity check, two fonts but neither supports characters used",
    fonts: [{ family: "a", src: "markA", descriptors: { unicodeRange: "u+41" }, loaded: false},
            { family: "a", src: "markB", descriptors: { unicodeRange: "u+42" }, loaded: false}],
    content: "CCC" },
  { test: "simple load sanity check, two fonts and both are used",
    fonts: [{ family: "a", src: "markA", descriptors: { unicodeRange: "u+41" }, loaded: true},
            { family: "a", src: "markB", descriptors: { unicodeRange: "u+42" }, loaded: true}],
    content: "ABC" },
  { test: "simple load sanity check, one with Han ranges",
    fonts: [{ family: "a", src: "markA", descriptors: { unicodeRange: "u+3???,u+5???,u+7???,u+8???" }, loaded: true},
            { family: "a", src: "markB", descriptors: { unicodeRange: "u+42" }, loaded: false}],
    content: "納豆嫌い" },
  { test: "simple load sanity check, two fonts with different styles A",
    fonts: [{ family: "a", src: "markA", descriptors: { unicodeRange: "u+41" }, loaded: true},
            { family: "a", src: "markB", descriptors: { weight: "bold", unicodeRange: "u+42" }, loaded: false}],
    content: "ABC" },
  { test: "simple load sanity check, two fonts with different styles B",
    fonts: [{ family: "a", src: "markA", descriptors: { weight: "bold", unicodeRange: "u+41" }, loaded: false},
            { family: "a", src: "markB", descriptors: { unicodeRange: "u+42" }, loaded: true}],
    content: "ABC" },
  { test: "multiple fonts with overlapping ranges, all with default ranges, only last one supports character used",
    fonts: [{ family: "a", src: "markC", descriptors: { }, loaded: true},
            { family: "a", src: "markA", descriptors: { }, loaded: true},
            { family: "a", src: "markB", descriptors: { }, loaded: true}],
    content: "CCC" },
  { test: "multiple fonts with overlapping ranges, all with default ranges, first one supports character used",
    fonts: [{ family: "a", src: "markB", descriptors: { }, loaded: false},
            { family: "a", src: "markA", descriptors: { }, loaded: false},
            { family: "a", src: "markC", descriptors: { }, loaded: true}],
    content: "CCC" },
  { test: "multiple fonts with overlapping ranges, one with default value in the fallback position",
    fonts: [{ family: "a", src: "markC", descriptors: { }, loaded: true},
            { family: "a", src: "markA", descriptors: { unicodeRange: "u+41" }, loaded: true},
            { family: "a", src: "markB", descriptors: { unicodeRange: "u+42" }, loaded: true}],
    content: "ABC" },
  { test: "multiple fonts with overlapping ranges, one with default value in the primary use position, fallback to one",
    fonts: [{ family: "a", src: "markA", descriptors: { unicodeRange: "u+41" }, loaded: true},
            { family: "a", src: "markB", descriptors: { unicodeRange: "u+42" }, loaded: false},
            { family: "a", src: "markC", descriptors: { }, loaded: true}],
    content: "AAA" },
  { test: "multiple fonts with overlapping ranges, one with default value in the primary use position, fallback to two",
    fonts: [{ family: "a", src: "markA", descriptors: { unicodeRange: "u+41" }, loaded: true},
            { family: "a", src: "markB", descriptors: { unicodeRange: "u+42" }, loaded: true},
            { family: "a", src: "markC", descriptors: { }, loaded: true}],
    content: "ABC" },
  { test: "multiple fonts with overlapping ranges, one with default value in the primary use position, no fallback",
    fonts: [{ family: "a", src: "markA", descriptors: { unicodeRange: "u+41" }, loaded: false},
            { family: "a", src: "markB", descriptors: { unicodeRange: "u+42" }, loaded: false},
            { family: "a", src: "markC", descriptors: { }, loaded: true}],
    content: "CCC" },
  { test: "metrics only case, ex-sized image, single font with space in range",
    fonts: [{ family: "a", src: "markA", descriptors: { unicodeRange: "u+0??" }, loaded: true}],
    content: "<img style='width: 2ex' src=\"" + redSquDataURL + "\">" },
  { test: "metrics only case, ex-sized image, single font with space outside range",
    fonts: [{ family: "a", src: "markA", descriptors: { unicodeRange: "u+1??" }, loaded: false}],
    content: "<img style='width: 2ex' src=\"" + redSquDataURL + "\">" },
  { test: "metrics only case, ch-sized image, single font with space in range",
    fonts: [{ family: "a", src: "markA", descriptors: { unicodeRange: "u+0??" }, loaded: true}],
    content: "<img style='width: 2ch' src=\"" + redSquDataURL + "\">" },
  { test: "metrics only case, ch-sized image, single font with space outside range",
    fonts: [{ family: "a", src: "markA", descriptors: { unicodeRange: "u+1??" }, loaded: false}],
    content: "<img style='width: 2ch' src=\"" + redSquDataURL + "\">" },
];

// map font loading descriptor names to @font-face rule descriptor names
var mapDescriptorNames = {
  style: "font-style",
  weight: "font-weight",
  stretch: "font-stretch",
  unicodeRange: "unicode-range",
  variant: "font-variant",
  featureSettings: "font-feature-settings"
};

var kBaseFontURL;
if ("SpecialPowers" in window) {
  kBaseFontURL = "";
} else {
  kBaseFontURL = "fonts/";
}

var mapFontURLs = {
  markA: "url(" + kBaseFontURL + "markA.woff" + ")",
  markB: "url(" + kBaseFontURL + "markB.woff" + ")",
  markC: "url(" + kBaseFontURL + "markC.woff" + ")",
  markD: "url(" + kBaseFontURL + "markD.woff" + ")",

  /* twourl versions include a bogus url followed by a valid url */
  markAtwourl: "url(" + kBaseFontURL + "bogus-markA.woff" + "), url(" + kBaseFontURL + "markA.woff" + ")",
  markBtwourl: "url(" + kBaseFontURL + "bogus-markB.woff" + "), url(" + kBaseFontURL + "markB.woff" + ")",
  markCtwourl: "url(" + kBaseFontURL + "bogus-markC.woff" + "), url(" + kBaseFontURL + "markC.woff" + ")",
  markDtwourl: "url(" + kBaseFontURL + "bogus-markD.woff" + "), url(" + kBaseFontURL + "markD.woff" + ")",

  /* localfont versions include a bogus local ref followed by a valid url */
  markAlocalfirst: "local(bogus-markA), url(" + kBaseFontURL + "markA.woff" + ")",
  markBlocalfirst: "local(bogus-markB), url(" + kBaseFontURL + "markB.woff" + ")",
  markClocalfirst: "local(bogus-markC), url(" + kBaseFontURL + "markC.woff" + ")",
  markDlocalfirst: "local(bogus-markD), url(" + kBaseFontURL + "markD.woff" + ")",
};

function familyName(name, i) {
  return "test" + i + "-" + name;
}

function fontFaceRule(name, fontdata, ft) {
  var desc = [];
  desc.push("font-family: " + name);
  var srckey = fontdata.src + ft;
  desc.push("src: " + mapFontURLs[srckey]);
  for (var d in fontdata.descriptors) {
    desc.push(mapDescriptorNames[d] + ": " + fontdata.descriptors[d]);
  }
  return "@font-face { " + desc.join(";") + " }";
}

function clearRules(sheetIndex) {
  var sheet = document.styleSheets[sheetIndex];
  while(sheet.cssRules.length > 0) {
    sheet.deleteRule(0);
  }
}

function clearAllRulesAndFonts() {
  clearRules(kSheetFonts);
  clearRules(kSheetStyles);
  document.fonts.clear();
}

function addStyleRulesAndText(testdata, i) {
  // add style rules for testcontent
  var sheet = document.styleSheets[kSheetStyles];
  while(sheet.cssRules.length > 0) {
    sheet.deleteRule(0);
  }
  var rule = [];
  var family = familyName(testdata.fonts[0].family, i);
  rule.push("#testcontent { font-family: " + family);
  if ("style" in testdata) {
    for (s in testdata.style) {
      rule.push(s + ": " + testdata.style[s]);
    }
  }
  rule.push("}");
  sheet.insertRule(rule.join("; "), 0);

  var content = document.getElementById("testcontent");
  content.innerHTML = testdata.content;
  content.offsetHeight;
}

// work arounds
function getFonts() {
  if ("forEach" in document.fonts) {
    return document.fonts;
  }
  return Array.from(document.fonts);
}

function getSize() {
  if ("size" in document.fonts) {
    return document.fonts.size;
  }
  return getFonts().length;
}

function getReady() {
  if (typeof(document.fonts.ready) == "function") {
    return document.fonts.ready();
  }
  return document.fonts.ready;
}

function setTimeoutPromise(aDelay) {
  return new Promise(function(aResolve, aReject) {
    setTimeout(aResolve, aDelay);
  });
}

function addFontFaceRules(testdata, i, ft) {
  var sheet = document.styleSheets[kSheetFonts];
  var createdFonts = [];
  testdata.fonts.forEach(function(f) {
    var n = sheet.cssRules.length;
    var fn = familyName(f.family, i);
    sheet.insertRule(fontFaceRule(fn, f, ft), n);
    var newfont;
    var fonts = getFonts();
    try {
      fonts.forEach(function(font) { newfont = font; });
      createdFonts.push({family: fn, data: f, font: newfont});
    } catch (e) {
      console.log(e);
    }
  });
  return createdFonts;
}

function addDocumentFonts(testdata, i, ft) {
  var createdFonts = [];
  testdata.fonts.forEach(function(fd) {
    var fn = familyName(fd.family, i);
    var srckey = fd.src + ft;
    var f = new FontFace(fn, mapFontURLs[srckey], fd.descriptors);
    document.fonts.add(f);
    createdFonts.push({family: fn, data: fd, font: f});
  });
  return createdFonts;
}

var q = Promise.resolve();

function runTests() {
  function setupTests() {
    setup({explicit_done: true});
  }

  function checkFontsBeforeLoad(name, testdata, fd) {
    test(function() {
      assert_equals(document.fonts.status, "loaded", "before initializing test, no fonts should be loading - found: " + document.fonts.status);
      var size = getSize();
      assert_equals(size, testdata.fonts.length,
                    "fonts where not added to the font set object");
      var i = 0;
      fonts = getFonts();
      fonts.forEach(function(ff) {
        assert_equals(ff.status, "unloaded", "added fonts should be in unloaded state");
      });
    }, name + " before load");
  }

  function checkFontsAfterLoad(name, testdata, fd, afterTimeout) {
    test(function() {
      assert_equals(document.fonts.status, "loaded", "after ready promise resolved, no fonts should be loading");
      var i = 0;
      fd.forEach(function(f) {
        assert_true(f.font instanceof FontFace, "font needs to be an instance of FontFace object");
        if (f.data.loaded) {
          assert_equals(f.font.status, "loaded", "font not loaded - font " + i + " " + f.data.src + " "
                        + JSON.stringify(f.data.descriptors) + " for content " + testdata.content);
        } else {
          assert_equals(f.font.status, "unloaded", "font loaded - font " + i + " " + f.data.src + " "
                        + JSON.stringify(f.data.descriptors) + " for content " + testdata.content);
        }
        i++;
      });
    }, name + " after load" + (afterTimeout ? " and timeout" : ""));
  }

  function testFontLoads(testdata, i, name, fd) {
    checkFontsBeforeLoad(name, testdata, fd);
    addStyleRulesAndText(testdata, i);

    var ready = getReady();
    return ready.then(function() {
      checkFontsAfterLoad(name, testdata, fd, false);
    }).then(function() {
      return setTimeoutPromise(0).then(function() {
        checkFontsAfterLoad(name, testdata, fd, true);
      });
    }).then(function() {
      var ar = getReady();
      return ar.then(function() {
        test(function() {
          assert_equals(document.fonts.status, "loaded", "after ready promise fulfilled once, fontset should not be loading");
          var fonts = getFonts();
          fonts.forEach(function(f) {
            assert_not_equals(f.status, "loading", "after ready promise fulfilled once, no font should be loading");
          });
        }, name + " test done check");
      });
    }).then(function() {
      clearAllRulesAndFonts();
    });
  }

  function testUnicodeRangeFontFace(testdata, i, ft) {
    var name = "TEST " + i + " " + testdata.test + " (@font-face rules)" + (ft != "" ? " " + ft : ft);

    var fd = addFontFaceRules(testdata, i, ft);
    return testFontLoads(testdata, i, name, fd);
  }

  function testUnicodeRangeDocumentFonts(testdata, i, ft) {
    var name = "TEST " + i + " " + testdata.test + " (document.fonts)" + (ft != "" ? " " + ft : ft);

    var fd = addDocumentFonts(testdata, i, ft);
    return testFontLoads(testdata, i, name, fd);
  }

  q = q.then(function() {
    setupTests();
  });

  var fontTypes = ["", "twourl", "localfirst"];

  unicodeRangeTests.forEach(function(testdata, i) {
    fontTypes.forEach(function(ft) {
      q = q.then(function() {
        return testUnicodeRangeFontFace(testdata, i, ft);
      }).then(function() {
        return testUnicodeRangeDocumentFonts(testdata, i, ft);
      });
    });
  });

  q = q.then(function() {
    done();
  });
}

if ("fonts" in document) {
  runTests();
} else {
  test(function() {
    assert_true(true, "CSS Font Loading API is not enabled.");
  }, "CSS Font Loading API is not enabled");
}
</script>
</body>
</html>
